/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.webos.java.base;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
import org.json.JSONObject;
import org.urchr.web.db.DBSession;
import org.webos.java.base.db.User;
import org.webos.java.base.workspace.Workspace;
import org.webos.java.explorer.Explorer;
import org.webos.java.explorer.ftypes.ApplicationFileProvider;
import org.webos.java.root.Log;
import org.webos.java.root.RandomGUID;
import org.webos.java.base.fs.vfs.VFSFileProvider;
import org.webos.java.base.users.UserProfile;

/**
 *
 * @author Kostya
 */
public class Session {

    private String id = RandomGUID.getGUID();
    public static final int SESSION_TIMEOUT = 45000;

    private String sessionKey = "";
    private int messageID = 0;
    List<Application> applications = new ArrayList<Application>();
    private List<Dialog> dialogs = new ArrayList<Dialog>();

    private Queue<JSONObject> messagesQueue = new LinkedList();
    private CoreApplication coreApplication = null;
    private String login = "";
    private String password = "";
    private String userCode = "";
    private boolean batchMode = false;
    private Date latestActivity = new Date();
    private DBFileProvider rootProvider = null;
    private VFSFileProvider vFSFileProvider = null;
    private ApplicationFileProvider applicationProvider = null;
    private boolean online = false;
    private boolean desktop = true;
    private Set<SessionStateListener> stateListeners = new HashSet<SessionStateListener>();
    private Explorer explorer;
    private String timeZoneName = "";
    private TimeZone timeZone = TimeZone.getDefault();

    public Session(String aKey, String aLogin, String aPassword, UserProfile aUserProfile) {
        setSessionKey(aKey);
        updateSession(aUserProfile);
        login = aLogin;
        password = aPassword;
        userCode = aUserProfile.getId();
        rootProvider = new DBFileProvider(this);
        vFSFileProvider = new VFSFileProvider(this);
        applicationProvider = new ApplicationFileProvider();
        coreApplication = (CoreApplication) startApplication(CoreApplication.class);
        explorer = (Explorer) startApplication(Explorer.class);
    }

    public void updateSession(UserProfile aUserProfile){
        timeZoneName = aUserProfile.getTimeZone();
        try {
//            timeZoneName = SystemManager.getProperty("default_time_zone", "Asia/Tokyo");
            Log.debug(this, "For user "+login+" timezone: "+timeZoneName);
            timeZone = TimeZone.getTimeZone(timeZoneName);
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }

    }

    public Explorer getExplorer() {
        return explorer;
    }

    public ApplicationFileProvider getApplicationProvider() {
        return applicationProvider;
    }

    public VFSFileProvider getVFSFileProvider() {
        return vFSFileProvider;
    }

    public DBFileProvider getRootProvider() {
        return rootProvider;
    }

    public CoreApplication getCoreApplication() {
        return coreApplication;
    }
    
    /**
     * @return the sessionKey
     */
    public String getSessionKey() {
        return sessionKey;
    }

    public void processMessage(JSONObject object, HttpServletResponse response) {
        latestActivity = new Date();
//        Log.debug(this, "Call "+object);
//        JSONObject resp = NetworkManager.getErrorObject(2, "Can't find message "+object.opt("method"));
        JSONObject resp = new JSONObject();
        resp.putOpt("type", "nope");
        Method[] methods = {};
        MessageHandler messageHandler = null;
        Object target = null;
        String d = object.optString("dialog");
        for (int i = 0; i < dialogs.size(); i++) {
            Dialog dialog = dialogs.get(i);
            if(dialog.getInstanceID()!=null 
                    && dialog.getInstanceID().equals(d)){
                target = dialog;
                break;
            }
        }

        for (int i = 0; i < applications.size(); i++) {
            Application app = applications.get(i);
            if(app.getCode()!=null && app.getCode().equals(object.opt("app"))){
                target = app;
                String frameID = object.optString("frame", null);
                if(frameID!=null){
                    for (int j = 0; j < app.getFramesCount(); j++) {
                        Frame frame = app.getFrameAt(j);
                        if(frame.getInstanceID().equals(frameID)){
                            target = frame;
                            messageHandler = frame.getHandler(object.optString("method"));
                            break;
                        }
                    }
                }
//                Log.debug(this, "Target: "+target.getClass().getSimpleName());
                break;
            }
        }
        if(target!=null){
//            Log.debug(this, "Call method "+object.opt("method")+" of "+
//                    target.getClass().getSimpleName());
            methods = target.getClass().getMethods();
            Method messageMethod = null;
            for (int j = 0; j < methods.length; j++) {
                Method method = methods[j];
                RemoteMessage mt = method.getAnnotation(RemoteMessage.class);
                if(mt!=null &&
                        ((mt.name()!=null && mt.name().equals(object.opt("method"))) ||
                        (mt.name().equals("") && method.getName().equals(object.opt("method"))))){
                    messageMethod = method;
                    break;
                }
            }
            if(messageMethod==null && messageHandler!=null){
                target = messageHandler.getInstance();
                messageMethod = messageHandler.getMethod();
            }
            if(messageMethod!=null){
                try {
                    JSONObject argument = object.optJSONObject("message");
                    if(argument==null)
                        argument = new JSONObject();
                    resp = (JSONObject) messageMethod.invoke(target, argument);
                    if(resp==null){
                        resp = new JSONObject();
                        resp.put("type", "nope");
                    }
                } catch (InvocationTargetException ex) {
                    Log.error(this, "Exception in message", object.opt("method"), ex.getTargetException());
                    ex.getTargetException().printStackTrace();
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        }
        //Have a chance to send messages from a queue
        JSONObject result = new JSONObject();
        try {
            result.put("message_result", resp);
            JSONArray arr = new JSONArray();
//            JSONObject m = null;
            //Disabled for now :(
//            while((m = messagesQueue.poll())!=null){
//                arr.put(m);
//            }
            result.put("messages", arr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        NetworkManager.sendJSON(response, result);
        try {
//            Log.debug(this, "Check full disable ", object.optBoolean("fullDisable", false));
            if(object.optBoolean("fullDisable", false)){
                JSONObject enable = new JSONObject();
                enable.put("type", "enable");
                enable.put("disableIndex", object.optInt("disableIndex", 0));
                Log.debug(this, "Send enable", enable);
                sendMessage(enable);
            }
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
    }

    synchronized public void sendMessage(JSONObject aObject){
        aObject.putOpt("messageID", messageID++);
        messagesQueue.offer(aObject);
        if(!batchMode)
            cancelLink();
    }

    /**
     * @param sessionKey the sessionKey to set
     */
    public void setSessionKey(String sessionKey) {
        this.sessionKey = sessionKey;
    }

    public void stopApplication(Application aApplication) {
        if(!applications.contains(aApplication))
            return;
        try {
            JSONObject object = new JSONObject();
            object.put("application", aApplication.getCode());
            getCoreApplication().sendMessage("removeApplication", object);
            aApplication.terminate();
            applications.remove(aApplication);
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
    }

    private synchronized void cancelLink(){
        try {
            notify();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    synchronized private JSONObject pollElement(){
        try {
            while(messagesQueue.size()>0){
                JSONObject obj = messagesQueue.poll();
                if(obj!=null)
                    return obj;
            }
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return null;
    }

    public synchronized void createLink(final HttpServletResponse response){
        latestActivity = new Date();
        setOnline(true, false);
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Date now = new Date();
//                Log.debug(this, "TimeTask result: "+(now.getTime()-latestActivity.getTime()));
                if(now.getTime()-latestActivity.getTime()>SESSION_TIMEOUT){
                    setOnline(false, false);
                }
            }
        }, SESSION_TIMEOUT+1000);
        String currentSession = sessionKey;
        cancelLink();
        JSONObject obj = pollElement();
        boolean afterSleep = obj==null;
        if(obj==null){
            try {
                wait(Constants.SESSION_LINK_TIMEOUT);
            } catch (InterruptedException ex) {}
            if(!currentSession.equals(sessionKey))
                return;
            obj = pollElement();
        }
        if(obj!=null){
            JSONArray messages = new JSONArray();
            do{
//                Log.debug(this, "Attaching: "+obj);
                messages.put(obj);
            }while((obj = pollElement())!=null);
            Log.debug(this, "Sending objects["+afterSleep+"]: "+messages.length());
            try {
                NetworkManager.sendJSON(response, messages);
            } catch (Exception e) {
                e.printStackTrace();
                reset();
            }
            return;
        }else{
            JSONObject object = new JSONObject();
            try {
                object.putOpt("type", "nope");
                NetworkManager.sendJSON(response, object);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    void resetMessages(){
        messagesQueue.clear();
        batchMode = false;
        cancelLink();
        messageID = 0;
    }

    public void reset(){
        sessionKey = "";
        resetMessages();
    }

    boolean announceApplication(Class<? extends Application> aClass){
        ApplicationType appInfo =  aClass.getAnnotation(ApplicationType.class);
        if(appInfo==null){
            Log.error(this, "Can't find information about "+aClass.getSimpleName());
            return false;
        }
        JSONObject startApp = new JSONObject();
        try {
            startApp.put("type", "start-app");
            startApp.put("code", appInfo.code());
            startApp.put("jsClass", appInfo.jsClass());
            startApp.put("description", appInfo.description());
            startApp.put("name", appInfo.name());
            startApp.putOpt("messageID", messageID++);
            sendMessage(startApp);
            return true;
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        
        return false;
    }

    public void announceApplication(Application app){
        announceApplication(app.getClass());
        for (int i = 0; i < app.getWorkspacesCount(); i++) {
            Workspace ws = app.getWorkspaceAt(i);
            getCoreApplication().sendAddWorkspace(ws);
        }
        for (int i = 0; i < app.getFramesCount(); i++) {
            Frame frame = app.getFrameAt(i);
            JSONObject obj = app.frameToJSON(frame);
//            Log.debug(this, "Frame data = "+frame.getData());
            if(frame.getData()!=null)
                obj.putOpt("data", frame.getData());
            getCoreApplication().sendMessage("addFrame", obj);
        }
        for(Dialog dialog: dialogs){
            if(app.equals(dialog.getApplication()))
                getCoreApplication().sendMessage("addDialog", dialogToJSON(dialog));

        }
//        getCoreApplication().sendMessage("finishLoading", new JSONObject());
    }

    public void announceApplications(){
        for (Application application : applications) {
            announceApplication(application.getClass());
            for (int i = 0; i < application.getWorkspacesCount(); i++) {
                Workspace ws = application.getWorkspaceAt(i);
                getCoreApplication().sendAddWorkspace(ws);
            }
            for (int i = 0; i < application.getFramesCount(); i++) {
                Frame frame = application.getFrameAt(i);
                JSONObject obj = application.frameToJSON(frame);
                if(frame.getData()!=null)
                    obj.putOpt("data", frame.getData());
                getCoreApplication().sendMessage("addFrame", obj);
            }
        }
        for(Dialog dialog: dialogs){
            getCoreApplication().sendMessage("addDialog", dialogToJSON(dialog));
        }
        getCoreApplication().sendMessage("finishLoading", new JSONObject());
    }

    public Application getApplication(Class<? extends Application> aClass){
        for (Application application : applications) {
            if(aClass.isAssignableFrom(application.getClass())){
                return application;
            }
        }
        return null;
    }

    public Application startApplication(Class<? extends Application> aClass){
        if(aClass==null)
            return null;
        for (Application application : applications) {
            if(aClass.isAssignableFrom(application.getClass())){
                Log.info(this, "Application "+aClass.getSimpleName()+" is already started");
                application.requestReOpen();
                return application;
            }
        }
        //Prepare and send startApp message
        //Read annotation
        ApplicationType appInfo =  aClass.getAnnotation(ApplicationType.class);
        if(appInfo==null){
            Log.error(this, "Can't find information about "+aClass.getSimpleName());
            return null;
        }
        Log.info(this, "Starting app "+appInfo.description()+"...");
        try {
            Constructor c = aClass.getConstructor(Session.class);
            if(c==null){
                return null;
            }
            announceApplication(aClass);
            Application app = (Application) c.newInstance(this);
//            app.setCode(appInfo.code());
            applications.add(app);
            app.initialize();
            return app;
        } catch (InvocationTargetException ite) {
            Log.error(this, "Exception in constructor: "+ite.getTargetException());
            ite.getTargetException().printStackTrace();

        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return null;
    }

    /**
     * @return the login
     */
    public String getLogin() {
        return login;
    }

    /**
     * @return the password
     */
    public String getPassword() {
        return password;
    }

    /**
     * @return the batchMode
     */
    public boolean isBatchMode() {
        return batchMode;
    }

    /**
     * @param batchMode the batchMode to set
     */
    public void setBatchMode(boolean batchMode) {
        this.batchMode = batchMode;
        if(!batchMode && !messagesQueue.isEmpty())
            cancelLink();
    }

    private JSONObject dialogToJSON(Dialog aDialog){
        try {
            DialogType dt = aDialog.getClass().getAnnotation(DialogType.class);
            JSONObject message = new JSONObject();
            message.put("app", aDialog.getApplication().getCode());
            message.put("dialog", aDialog.getInstanceID());
            if(dt!=null)
                message.put("jsClass", dt.jsClass());
            message.put("object", aDialog.getData());
            return message;
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return null;
    }

    public boolean addDialog(Dialog aDialog){
        try {
            if(dialogs.contains(aDialog)){
                return false;
            }
            dialogs.add(aDialog);
            if(getCoreApplication()!=null){
                getCoreApplication().sendMessage("addDialog", dialogToJSON(aDialog));
                return true;
            }
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return false;
    }

    public boolean removeDialog(Dialog aDialog){
        try {
            if(!dialogs.contains(aDialog))
                return false;
            dialogs.remove(aDialog);
            JSONObject message = new JSONObject();
            message.put("dialog", aDialog.getInstanceID());
            Log.debug(this, "Removing dialog "+message);
            if(getCoreApplication()!=null)
                getCoreApplication().sendMessage("removeDialog", message);
            return true;
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return false;
    }

    public int getDialogCount(){
        return dialogs.size();
    }

    public Dialog getDialogAt(int i){
        return dialogs.get(i);
    }

    /**
     * @return the latestActivity
     */
    public Date getLatestActivity() {
        return latestActivity;
    }

    /**
     * @return the userCode
     */
    public String getUserCode() {
        return userCode;
    }

    /**
     * @param userCode the userCode to set
     */
    public void setUserCode(String userCode) {
        this.userCode = userCode;
    }

    public User getUser(DBSession aSession){
        try {
            User user = (User) aSession.createQuery("from User where code=:code").
                    setString("code", getUserCode()).uniqueResult();
            if(user!=null)
                return user;
            DBSession dbs = new DBSession();
            try {
                dbs.begin();
                user = new User();
                user.setCode(getUserCode());
                dbs.save(user);
                dbs.commit();
                return getUser(aSession);
            } catch (Throwable t) {
                dbs.rollback();
                Log.error(this, "Exception thrown: " + t.getMessage());
                t.printStackTrace();
            } finally {
                dbs.close();
            }
        } catch (Throwable t) {
            Log.error(this, "Exception thrown: " + t.getMessage());
            t.printStackTrace();
        }
        return null;
    }

    void setOnline(boolean aOnline, boolean aForceSend){
        if(aOnline!=online || aForceSend){
            for (SessionStateListener sessionStateListener : stateListeners) {
                if(aOnline)
                    sessionStateListener.sessionOnline();
                else
                    sessionStateListener.sessionOffline();
            }
            online = aOnline;
        }
    }

    public void addSessionStateListener(SessionStateListener aListener){
        stateListeners.add(aListener);
    }

    public void removeSessionStateListener(SessionStateListener aListener){
        stateListeners.remove(aListener);
    }

    /**
     * @return the desktop
     */
    public boolean isDesktop() {
        return desktop;
    }

    /**
     * @param desktop the desktop to set
     */
    public void setDesktop(boolean desktop) {
        this.desktop = desktop;
    }

    public String getId() {
        return id;
    }

    public String normalizeDate(Date aDate, DateFormat aDateFormat){
        Date norm = normalizeDate(aDate);
        if(norm==null || aDateFormat==null)
            return "";
        return aDateFormat.format(norm);
    }

    public Date normalizeDate(Date aDate){

        if(aDate==null)
            return null;
        Calendar c = Calendar.getInstance();
        c.setTime(aDate);
        c.add(Calendar.MILLISECOND, timeZone.getOffset(aDate.getTime())-
                TimeZone.getDefault().getOffset(aDate.getTime()));
        return c.getTime();
    }

    public TimeZone getTimeZone() {
        return timeZone;
    }

    void setPassword(String password) {
        this.password = password;
    }


}
